跳到主要内容

SpringBoot 整合 SpringMVC

前后端分离后 MVC 的理解

参考资料 前后端分离理论 - 七夜的文章

因为学习 SpringBoot 之前学习了一段时间的前端,主要是 Vue 框架以及基于 NodeJS 的后端框架 Express,所以也能比较好的理解前后端分离这个概念。

学完回来继续学习 SpringBoot 总是不免要拿出之前学习的 SpringMVC 出来进行比较,SpringMVC 是一个 Spring 用来处理 MVC(Model、View、Controller) 分层的一个模块,主要的疑惑点就在于这个 View 的理解,在 Spring 开始学习的阶段还没有完全的前后端分离,动态页面还是通过 JSP 来完成的,所以这个 View 当时的理解就是单纯的把 Model 传递给 JSP 页面进行动态更新。

MVC 作用回顾

Model:就是一个个模块,用于处理某个具体的业务,而无需直面用户的具体请求

View:视图层,展示给用户的,这个就不解释了

Controller:接收用户的请求并去调用具体的某个模块来处理用户的需求,然后把数据返回给 View 层

MVC 如何前后端解耦

但是到了前后端分离的现在 MVC 感觉就已经显得非常耦合了,这个 MVC 如何理解呢?

实际上,上面的 MVC 理解不太正确, View 并非指的是 JSP 而是指视图层,所以这个 View 可以是浏览器(如果将浏览器一端视为前端,而服务器一端视为后端的话)

aR0URU.png

View 可以是指浏览器,而这个 View 转移到浏览器的过程就是通过 REST(REpresentational State Transfer:表现层状态转移)完成的(就是用一个规范的 API 来完成以前直接通过 JSP 进行的操作)

aR0gJK.png

REST

参考资料 怎样用通俗的语言解释REST,以及RESTful? - 覃超的回答

实际上就是一套接口规范,用HTTP动词(GET、POST、DELETE、DETC)来描述要进行的操作

REST描述的是在网络中 client 和 server 的一种交互形式;REST本身不实用,实用的是如何设计 RESTful API(REST风格的网络接口)

整合 SpringMVC

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>

端口和静态资源

端口

# 修改端口号
server.port=8080

静态资源

resources 目录下的 static 文件夹下存放静态资源

配置拦截器

1、先创建一个拦截器(就是实现 HandlerInterceptor 接口) 里面有三个方法:preHandle、postHandle、afterCompletion

preHandle 方法的 return true 表示放行,执行下一个拦截器

  • preHandle:访问方法前执行
  • postHandle:访问完方法后执行(显示视图前)
  • afterCompletion:访问完方法后执行(显示视图后)
@Slf4j
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 这个 log 是上面那个 @Slf4j 注解注入的(Lombok插件)
log.debug("这是 preHandle");
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.debug("这是 postHandle");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.debug("这是 afterCompletion");
}
}

2、编写配置类实现 WebMvcConfigurer 接口

@Configuration
public class MvcConfig implements WebMvcConfigurer {
// 注册拦截器 MyInterceptor 前面自定义的 拦截器
@Bean
public MyInterceptor myInterceptor(){
return new MyInterceptor();
}


// 添加拦截器到 Spring MVC 拦截器链 里面
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 拦截所有路径 /** 表示这个请求下的所有请求 /* 只拦截一级请求
registry.addInterceptor(myInterceptor()).addPathPatterns("/**");
// 拦截器顺序从上到下
registry.addInterceptor(myInterceptor()).addPathPatterns("/**");
}
}

@RestController

就是 @Controller@ResponseBody 的整合版 @ResponseBody 就是把响应体直接返回出去,而不是让每个 server 返回一个视图对象

一般就是使用这个用来标注类,因为前后端之后,后端的任务存粹到只需返回数据给前端

配置原生 Servlet

全局访问路径

默认情况下,在 Spring Boot 中内置服务器的默认访问路径是:/

而在 Controller 的类上配置 @RequestMapping 注解也只能配置当前类下的访问路径的父路径,如果要配置全局访问路径需要在配置文件上修改

# Context-Path 是返回的项目上下文的名字
# 如果 server.servlet.context-path 没有配,请求的url地址就是 localhost : port/temp/convert
# 如果 server.servlet.context-path = "/market/task", 请求的url地址就是 localhost : port/market/task/temp/convert
# 如下的访问根路径为 http://127.0.0.1:8080/path
server:
servlet:
context-path: /path

# 这个 Servlet-Path 是设置调度服务(dispatcher servlet)的 path,但是不知道为啥好像配置了没什么卵用...
# 所以只需配置上面那个就行了
spring:
mvc:
servlet:
path: /path02

添加过滤器

@Slf4j
@WebFilter(filterName = "JwtFilter", urlPatterns = "/user/*")
public class JwtFilter implements Filter {

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;

response.setContentType("application/json; charset=UTF-8");
//获取 header里的token
final String token = request.getHeader("authorization");

if (token == null) {
response.getWriter().write("no token");
return;
}

DecodedJWT verifier = JWTUtils.verifier(token);

if (verifier == null) {
response.getWriter().write("authentication failure!!");
return;
}

Claim username = verifier.getClaims().get("username");
request.setAttribute("userName", username);
chain.doFilter(req, res);
}
}

因为这个 @WebFilter 是原生的注解 Servlet,所以要在 Application 类上加上 @ServletComponentScan 注解,表示扫描包内的原生注解

JSON 解析

JSONObject 的数据解析参考:JSON数据解析只要看这个就够了 例如:

{
"personData": [
{
"age": 12,
"name": "nate",
"schoolInfo": [
{
"School_name": "清华"
},
{
"School_name": "北大"
}
],
"url": "https://image.alsritter.icu/uploadImages/2014/345/36/E8C039MU0180.jpg"
},
{
"age": 24,
"name": "jack",
···
}
],
"result": 1
}

解析:

JSONArray jsonArray = jsonObject.getJSONArray("personData");
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject personData = jsonArray.getJSONObject(i);
int age = personData.getInt("age");
String url = personData.getString("url");
String name = personData.getString("name");
···
}

JSON 编码问题

只需在 MvcConfig 配置文件上设置 json 对象处理器的编码

// 设置编码
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
jsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
return jsonConverter;
}

// 在消息转换器里添加这个响应处理器
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
WebMvcConfigurer.super.configureMessageConverters(converters);
converters.add(mappingJackson2HttpMessageConverter());
}

全局异常处理

public class MyResponseErrorHandler extends DefaultResponseErrorHandler {

@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return super.hasError(response);
}

@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
throw new UnknownHttpStatusCodeException(response.getRawStatusCode(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
}
handleError(response, statusCode);
}

@Override
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
switch (statusCode.series()) {
case CLIENT_ERROR:
HttpClientErrorException exp1 = new HttpClientErrorException(statusCode, response.getStatusText(), response.getHeaders(), getResponseBody(response), getCharset(response));
logger.error("客户端调用异常",exp1);
throw exp1;
case SERVER_ERROR:
HttpServerErrorException exp2 = new HttpServerErrorException(statusCode, response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
logger.error("服务端调用异常",exp2);
throw exp2;
default:
UnknownHttpStatusCodeException exp3 = new UnknownHttpStatusCodeException(statusCode.value(), response.getStatusText(),
response.getHeaders(), getResponseBody(response), getCharset(response));
logger.error("网络调用未知异常");
throw exp3;
}
}

}

HttpMessageConverter 的方式

自定义转换器

参考资料 自定义HttpMessageConverter

HttpMessageConverter 是用来处理 request 和 response 里的数据的

调用 Restful 接口传递的数据内容是 Json 格式的字符串,返回的响应也是 Json 格式的字符串。然而 restTemplate.postForObject 方法的请求参数 RequestBean 和返回参数 ResponseBean 却都是 Java类。是 RestTemplate 通过 HttpMessageConverter 自动帮我们做了转换的操作。

默认情况下 RestTemplate 自动帮我们注册了一组 HttpMessageConverter 用来处理一些不同的 contentType 的请求。

如 StringHttpMessageConverter 来处理 text/plain; MappingJackson2HttpMessageConverter 来处理 application/json; MappingJackson2XmlHttpMessageConverter 来处理 application/xml

可以在 org.springframework.http.converter 包下找到所有 Spring 帮我们实现好的转换器。

public class SettingConverter extends AbstractHttpMessageConverter<SettingInfoRequest> {
/**
* 定义字符编码,防止乱码
*/
private static final Charset DEFAULT_CHARSET = Charsets.UTF_8;

/**
* 新建自定义的媒体类型
*/
public SettingConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}

/**
* 表明只处理Settings这个类
*/
@Override
protected boolean supports(Class<?> aClass) {
return SettingInfoRequest.class.isAssignableFrom(aClass);
}

/**
* 重写readInternal方法,处理请求的数据
*/
@Override
protected SettingInfoRequest readInternal(Class<? extends SettingInfoRequest> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
final String temp = StreamUtils.copyToString(httpInputMessage.getBody(), DEFAULT_CHARSET);
if (temp.contains(TRAINING.name())) {
return JSONObject.parseObject(temp, new TypeReference<SettingInfoRequest<SettingInfo>>(){});
} else if (temp.contains(MODELDUMP.name())) {
return JSONObject.parseObject(temp, new TypeReference<SettingInfoRequest<DumpSettings>>(){});
}
return null;
}

/**
* 重写writeInternal,处理如何输出数据到response
*/
@Override
protected void writeInternal(SettingInfoRequest request, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
final String out = FastJsonUtils.toJSONString(request);
StreamUtils.copy(out, DEFAULT_CHARSET, httpOutputMessage.getBody());
}
}

配置自定义 Converter

@Configuration
public class AlphaWebConfig extends WebMvcConfigurerAdapter {

/**
* 添加自定义的httpMessageConverter
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(messageConverter());
}

@Bean
public SettingConverter messageConverter(){
return new SettingConverter();
}

}

自定义类型转换器的方式

SpringMVC 默认已经提供了一些常用的类型转换器,例如客户端提交的字符串能自动转成 int 类型再传入形参

但是有些还需要自定义的转换器,例如传入一个日期的数据进来(例如这篇博客用的日期就是 2020-08-16)这种情况就需要自定义类型转换器

注意:因为传入的数据全都是字符串,所以类型转换器的策略一般是优先匹配自定义的类型转换器,匹配失败换下一个转换器(官方默认的),全部转换器都匹配失败则报错

自定义类型转换器的开发步骤

1、定义转换器类实现 Converter 接口

2、在配置文件中声明转换器

3、在 <annotation-driven> (注解驱动)中引用转换器

创建 Converter 实现类

public class DataConverter implements Converter<String, Date> {
@Override
public Date convert(String source) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
Date date = null;
try {
date = format.parse(source);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}

声明这个 Converter

<!-- 注意:这个 mvc:annotation-driven 的支持是 
xmlns:mvc="http://www.springframework.org/schema/mvc" 别引用错了 -->

<!--声明转换器-->
<bean id="ConversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<list>
<bean class="com.alsritter.converter.DataConverter"/>
</list>
</property>
</bean>

<!-- 把这个转换器加入到驱动里 -->
<mvc:annotation-driven conversion-service="ConversionService"/>

HttpMessageConverter 和 Converter 的区别

参考资料 HttpMessageConverter和Converter的区别

上面讲了两种自定义转换器的方式,它们有什么区别呢?

Spring3 引入了 Convert/Format SPI,Convert SPI可以实现任意 Java类型的转换;Format SPI 支持国际化,并在前者的基础上实现了 String 与任意类型的转换。这两类 SPI 属于 spring-core,被整个 spring-framework 共享,是一种通用的类型转换器。

而 HttpMessageConverter 属于spring-web,HttpMessageConverter 是将 HTTP 请求内容转换成 Java对象,以及将 Java对象转换成 HTTP 响应内容,说白了就是对 HTTP 的反序列化和序列化,它的仅仅被用于 HTTP 主体和 Java 对象之间的转换。

SpringMVC 默认会注册一些自带的 HttpMessageConvertor(从先后顺序排列分别为 ByteArrayHttpMessageConverter、StringHttpMessageConverter、ResourceHttpMessageConverter、SourceHttpMessageConverter、AllEncompassingFormHttpMessageConverter)

所以它们的核心区别就是 HttpMessageConverter 是一个消息转换器,而 Convert 是数据转换器,具体区别看 Spring 数据转换那篇笔记

@CrossOrigin 跨域

controller 方法的 CORS配置,可以向 @RequestMapping 注解处理程序方法添加一个 @CrossOrigin 注解,以便启用 CORS(默认情况下,@CrossOrigin 允许在 @RequestMapping 注解中指定的所有源和 HTTP 方法):

// 或者直接加在类上全局跨域
@RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

其中 @CrossOrigin 中的 2个参数:

  • origins :允许可访问的域列表
  • maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。

为整个 controller 启用 @CrossOrigin

@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

在这个例子中,对于 retrieve()remove() 处理方法都启用了跨域支持,还可以看到如何使用 @CrossOrigin 属性定制 CORS 配置。

全局 CORS 配置

除了细粒度、基于注释的配置之外,还可能需要定义一些全局 CORS 配置。这类似于使用筛选器,但可以声明为 Spring MVC 并结合细粒度 @CrossOrigin 配置。默认情况下,所有 origins and GET, HEAD and POST methods 是允许的。

JavaConfig,使整个应用程序的 CORS 简化为:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}

如果正在使用 Spring Boot,建议将 WebMvcConfigurer bean 声明如下:

@Configuration
public class MyConfiguration {

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
};
}
}

可以轻松地更改任何属性,以及仅将此 CORS 配置应用到特定的路径模式:

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(false).maxAge(3600);
}